﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.Configuration;
using System.IO;
using System.Diagnostics;
using Newtonsoft.Json;

namespace WU.LoadTester.Lib.Service
{
    public enum BindingType
    {
        None,
        BasicHttpBinding,
        WsHttpBinding,
        NetTCPBinding,
        AsmxBinding
    }

    public class Endpoint : IDisposable
    {
        private object _client;
        private Binding _binding;
        private EndpointAddress _address;
        private AuthSettings _auth;
        private Uri _url;
        private string _editor;


        /// <summary>
        /// Setup the service client
        /// </summary>
        public Endpoint(string address, string editor, BindingType binding)
        {
            this.ConfigureBinding(binding);
            _address = new EndpointAddress(address);
            _client = DynamicProxy.GetClientClass(address, _address, _binding);
            OpenClient();
        }

        /// <summary>
        /// Setup a Web Client
        /// </summary>
        public Endpoint(string address, string editor, AuthSettings authSettings)
        {
           if(!Uri.TryCreate(address, UriKind.Absolute, out _url))
           {
               throw new Exception(Resources.Error_RequestUrlInvalid);
           }
            _auth = authSettings;
        }

        /// <summary>
        /// Set the client bindings and address. Open the client connection
        /// </summary>
        private void OpenClient()
        {
            //Open the connection
            var openMethod = _client.GetType().GetMethod("Open");
            var state = _client.GetType().GetProperty("State");
            if (state == null || openMethod == null)
                throw new Exception(Resources.Error_OpenMethodNotFound);

            var stateValue = (CommunicationState) state.GetValue(_client);
            if (stateValue != CommunicationState.Opened || stateValue != CommunicationState.Opening)
            {
                openMethod.Invoke(_client, null);
            }
        }

        /// <summary>
        /// Close the client connection
        /// </summary>
        private void CloseClient()
        {
            var closeMethod = _client.GetType().GetMethod("Close");
            closeMethod.Invoke(_client, null);
        }

        /// <summary>
        /// Call the specified service method
        /// </summary>
        /// <param name="methodName">Name of the exposed method to call</param>
        /// <param name="parameters">Parameters of the method</param>
        /// <returns></returns>
        public Result Call(string methodName, IEnumerable<string> parameters)
        {
            //Find the method
            MethodInfo methodInfo = _client.GetType().GetMethods().FirstOrDefault(method => methodName == method.Name);

            if (methodInfo == null)
                return new Result(Resources.Error_MethodNotFound, false, TimeSpan.FromSeconds(1));

            //Build the request with the correct parameters
            var paramInfo = methodInfo.GetParameters();
            int count = parameters.Count();
            var objParams = new object[count];

            //Check if the parameter count match
            if (paramInfo.Count() != count)
                throw new Exception(
                    String.Format(Resources.Error_ParameterMismatch,
                                  methodInfo.Name));

            //Build the parameters object array
            for (int i = 0; i < count; i++)
            {
                var objParam = this.BuildParameter(parameters.ElementAt(i), paramInfo[i]);
                objParams[i] = objParam;
            }

            //Execute the method
            var dt = DateTime.Now;
            var callResult = methodInfo.Invoke(_client, objParams);
            TimeSpan calltime = DateTime.Now.Subtract(dt);
            return ExtractResult(callResult, callResult.GetType(), calltime);
        }

        /// <summary>
        /// Call the service method async
        /// </summary>
        /// <param name="methodName">Name of the exposed method to call</param>
        /// <param name="parameters">Parameters of the method</param>
        /// <returns></returns>
        public async Task<Result> CallAsync(string methodName, IEnumerable<string> parameters)
        {
            //Find the method
            var methods = _client.GetType().GetMethods();
            MethodInfo methodInfo = methods.FirstOrDefault(method => (methodName + "Async") == method.Name);

            if (methodInfo == null)
                return new Result(Resources.Error_MethodNotFound, false, TimeSpan.FromSeconds(1));

            //Build the request with the correct parameters
            var paramInfo = methodInfo.GetParameters();
            int count = parameters.Count();
            var objParams = new object[count];

            //Check if the parameter count match
            if (paramInfo.Count() != count)
                throw new Exception(
                    String.Format(Resources.Error_ParameterMismatch,
                                  methodInfo.Name));

            //Build the parameters object array
            for (int i = 0; i < count; i++)
            {
                try
                {
                    var objParam = this.BuildParameter(parameters.ElementAt(i), paramInfo[i]);
                    objParams[i] = objParam;
                }
                catch
                {
                    throw new Exception(String.Format(Resources.Error_ParameterBuild, paramInfo[i].Name));
                }
            }

            //Execute the method
            var dt = DateTime.Now;
            var callResult = await (dynamic) methodInfo.Invoke(_client, objParams);
            TimeSpan calltime = DateTime.Now.Subtract(dt);
            return ExtractResult(callResult, callResult.GetType(), calltime);
        }

        /// <summary>
        /// Call a web client
        /// </summary>
        public async Task<Result> WebCall(string method, IEnumerable<string> parameters)
        {
            var result = new Result();

            if(parameters.Count() > 2)
                throw new NotSupportedException(Resources.Error_RestParameterExceedsOne);

            if (parameters.Count() == 2 && parameters.ElementAt(1) != string.Empty)
            {
                //Build JSON object
                var sb = new StringBuilder();
                var sw = new StringWriter(sb);
                var writer = new JsonTextWriter(sw);

                try
                {
                    this.BuildJsonParameter(parameters.ElementAt(1), writer);
                }
                catch(Exception ex)
                {
                    throw new FormatException(Resources.Error_ParameterFormat, ex);
                }

                //Hit relative URL with parameter

                var request = new JsonRequest(_url.AbsoluteUri);
                var dt = DateTime.Now;
                result.Message = await request.ExecuteAsync(parameters.ElementAt(0), sb.ToString(), method);
                result.ResponseTime = DateTime.Now.Subtract(dt);
                result.Success = true;
                return result;
            }
            if(parameters.Count() == 1)
            {
                //Hit the relative URL
                var request = new JsonRequest(_url.AbsoluteUri);
                var dt = DateTime.Now;
                result.Message = await request.ExecuteAsync(parameters.ElementAt(0), method);
                result.ResponseTime = DateTime.Now.Subtract(dt);
                result.Success = true;
                return result;
            }
            else
            {
                //Hit the URL directly
                var request = new JsonRequest(_url.AbsoluteUri);
                var dt = DateTime.Now;
                result.Message = await request.ExecuteAsync();
                result.ResponseTime = DateTime.Now.Subtract(dt);
                result.Success = true;
                return result;
            }
        }


        private void BuildJsonParameter(string value, JsonWriter writer)
        {
            var type  = ValueType.Base;
            if(value.StartsWith("["))
            {
                type = ValueType.Collection;
                writer.WriteStartArray();
            }
            else if(value.StartsWith("{"))
            {
                type = ValueType.Object;
                writer.WriteStartObject();
            }
            else
            {
                writer.WriteValue(this.ConvertParameter(value, typeof(string)));
                return;
            }

            var fields = this.GetFields(value, type);
            foreach(var field in fields)
            {
                if (type == ValueType.Object)
                {
                    var fieldValues = field.Split(new char[] { ':' }, 2);
                    var fieldName = fieldValues[0].Trim();
                    var fieldValue = fieldValues[1].TrimEnd(';');

                    writer.WritePropertyName(fieldName);
                    this.BuildJsonParameter(fieldValue, writer);
                }
                else
                {
                    this.BuildJsonParameter(field, writer);
                }
            }

            if(type == ValueType.Collection)
                writer.WriteEndArray();
            else if(type == ValueType.Object)
                writer.WriteEndObject();
        }


        private object BuildParameter(string value, ParameterInfo info)
        {
            if (value.StartsWith("{"))
            {
                //value is an object
                char[] separtors2 = {':'};
                var fields = this.GetFields(value, ValueType.Object);
                object returnValue = Activator.CreateInstance(info.ParameterType);
                foreach (var field in fields)
                {
                    var fieldValues = field.Split(separtors2, 2);
                    var fieldName = fieldValues[0].Trim();
                    var fieldValue = fieldValues[1].TrimEnd(';');


                    var p = returnValue.GetType().GetProperty(fieldName);
                    //Subvalue is an object or a collection, call resursive
                    p.SetValue(returnValue,
                               fieldValue.StartsWith("{") || fieldValue.StartsWith("[")
                                   ? BuildSubParameter(fieldValue, p)
                                   : ConvertParameter(fieldValue, p.PropertyType));
                }
                return returnValue;
            }
            else if (value.StartsWith("["))
            {
                //Value is a collection
                var fields = this.GetFields(value, ValueType.Collection);
                bool isArray = info.ParameterType.IsArray;
                Type underlyingType = null;
                underlyingType = isArray
                                     ? info.ParameterType.GetElementType()
                                     : info.ParameterType.GenericTypeArguments[0];

                var returnType = typeof (List<>).MakeGenericType(underlyingType);
                object returnValue = Activator.CreateInstance(returnType);
                var methodAdd = returnType.GetMethod("Add");

                //Add each item in the collection
                foreach (var field in fields)
                {
                    var subObject = this.BuildSubParameter(field, underlyingType);
                    methodAdd.Invoke(returnValue, new[] {subObject});
                }

                if (isArray)
                {
                    return returnType.GetMethod("ToArray").Invoke(returnValue, null);
                }
                return returnValue;
            }
            else
            {
                //Value is a basic type
                return ConvertParameter(value, info.ParameterType);
            }
        }

        private object BuildSubParameter(string value, Type type)
        {
            if (value.StartsWith("{"))
            {
                //value is an object
                char[] separtors2 = {':'};
                var fields = this.GetFields(value, ValueType.Object);

                object returnValue = Activator.CreateInstance(type);
                foreach (var field in fields)
                {
                    var fieldValues = field.Split(separtors2, 2);
                    var fieldName = fieldValues[0].Trim();
                    var fieldValue = fieldValues[1].TrimEnd(';');


                    var p = returnValue.GetType().GetProperty(fieldName);
                    //Subvalue is an object or a collection, call resursive
                    p.SetValue(returnValue,
                               fieldValue.StartsWith("{") || fieldValue.StartsWith("[")
                                   ? BuildSubParameter(fieldValue, p)
                                   : ConvertParameter(fieldValue, p.PropertyType));
                }

                return returnValue;
            }
            else if (value.StartsWith("["))
            {
                //Value is a collection
                var fields = this.GetFields(value, ValueType.Collection);
                bool isArray = type.IsArray;
                Type underlyingType = null;
                underlyingType = isArray ? type.GetElementType() : type.GenericTypeArguments[0];

                var returnType = typeof (List<>).MakeGenericType(underlyingType);
                object returnValue = Activator.CreateInstance(returnType);
                var methodAdd = returnType.GetMethod("Add");

                //Add each item in the collection
                foreach (var field in fields)
                {
                    var subObject = this.BuildSubParameter(field, underlyingType);
                    methodAdd.Invoke(returnValue, new[] {subObject});
                }

                if (isArray)
                {
                    return returnType.GetMethod("ToArray").Invoke(returnValue, null);
                }

                return returnValue;
            }
            else
            {
                return ConvertParameter(value, type);
            }
        }

        private object BuildSubParameter(string value, PropertyInfo info)
        {
            if (value.StartsWith("{"))
            {
                //value is an object
                char[] separators = {';'};
                char[] separtors2 = {':'};
                var fields = this.GetFields(value, ValueType.Object);

                object returnValue = Activator.CreateInstance(info.PropertyType);
                foreach (var field in fields)
                {
                    var fieldValues = field.Split(separtors2, 2);
                    var fieldName = fieldValues[0].Trim();
                    var fieldValue = fieldValues[1].TrimEnd(';');


                    var p = returnValue.GetType().GetProperty(fieldName);
                    //Subvalue is an object or a collection, call resursive
                    p.SetValue(returnValue,
                               fieldValue.StartsWith("{") || fieldValue.StartsWith("[")
                                   ? BuildSubParameter(fieldValue, p)
                                   : ConvertParameter(fieldValue, p.PropertyType));
                }

                return returnValue;
            }
            else if (value.StartsWith("["))
            {
                //Value is a collection
                var fields = this.GetFields(value, ValueType.Collection);
                bool isArray = info.PropertyType.IsArray;
                Type underlyingType = null;
                underlyingType = isArray
                                     ? info.PropertyType.GetElementType()
                                     : info.PropertyType.GenericTypeArguments[0];

                var returnType = typeof (List<>).MakeGenericType(underlyingType);
                object returnValue = Activator.CreateInstance(returnType);
                var methodAdd = returnType.GetMethod("Add");

                //Add each item in the collection
                foreach (var field in fields)
                {
                    var subObject = this.BuildSubParameter(field, underlyingType);
                    methodAdd.Invoke(returnValue, new[] {subObject});
                }

                if (isArray)
                {
                    return returnType.GetMethod("ToArray").Invoke(returnValue, null);
                }

                return returnValue;
            }
            else
            {
                return ConvertParameter(value, info.PropertyType);
            }
        }

        private object ConvertParameter(string value, Type type)
        {
            if (type == typeof (int))
            {
                return int.Parse(value);
            }
            if (type == typeof (double))
            {
                return double.Parse(value);
            }
            if (type == typeof (decimal))
            {
                return decimal.Parse(value);
            }
            if (type == typeof (bool))
            {
                return bool.Parse(value);
            }
            if (value == "null")
            {
                return null;
            }
            return Convert.ChangeType(value, type);
        }

        private Result ExtractResult(object result, Type resultType, TimeSpan time)
        {
            var returnResult = new Result();
            returnResult.ResponseTime = time;
            returnResult.Success = true;

            if (IsBaseType(resultType))
            {
                returnResult.Message = result.ToString();
            }
            else
            {
                var properties = resultType.GetProperties();
                foreach (var property in properties)
                {
                    if (IsBaseType(property.PropertyType))
                    {
                        try
                        {
                            returnResult.Message += String.Format("{0} : {1},", property.Name, property.GetValue(result));
                        }
                        catch
                        {
                            returnResult.Message += String.Format("{0}: {1},", property.Name,
                                                                  Resources.Error_ExtractResult);
                        }
                    }
                }
            }
            return returnResult;
        }


        /// <summary>
        /// Generate a configuration file with bindings section and a binding element from the type selected.
        /// Promp the user to add or modify it's binding configuration.
        /// </summary>
        /// <param name="binding"></param>
        private void ConfigureBinding(BindingType binding)
        {
            //Generate temporary configuration files load them in configuration manager
            string tempName = Path.GetTempPath() + Path.GetRandomFileName() + ".exe";
            string configName = tempName + ".config";
            var exe = new StreamWriter(tempName, false, Encoding.UTF8);
            exe.Dispose();
            var config = new StreamWriter(configName, false, Encoding.UTF8);

            //Dump const XML depending on selected binding
            config.Write(Bindings.BindingConstants.BindingConfigurationHeader);
            config.WriteLine(Bindings.BindingConstants.BindingComments);

            switch (binding)
            {
                case BindingType.BasicHttpBinding:
                    config.Write(Bindings.BindingConstants.BasicHttpBindingConfiguration);
                    break;
                case BindingType.WsHttpBinding:
                    config.Write(Bindings.BindingConstants.WsHttBindingConfiguration);
                    break;
                case BindingType.NetTCPBinding:
                    config.Write(Bindings.BindingConstants.NetTCPBindingConfiguration);
                    break;
                case BindingType.AsmxBinding:
                    config.Write(Bindings.BindingConstants.CustomASMXBindingConfiguration);
                    break;
            }
            config.Write(Bindings.BindingConstants.BindingConfigurationFooter);
            config.Flush();
            config.Dispose();

            //Prompt for edition and wait
            var process = Process.Start(this._editor, configName);
            if(process == null)
                throw new FileNotFoundException(Resources.Error_NotePadMissing, Resources.Error_NotePadExe);
            
            process.WaitForExit();

            //Load configuration
            var configuration = ConfigurationManager.OpenExeConfiguration(tempName);
            var bindingSection = BindingsSection.GetSection(configuration);

            //Create the binding object
            switch (binding)
            {
                case BindingType.BasicHttpBinding:
                    _binding = new Bindings.BasicHttpBinding(bindingSection.BasicHttpBinding.Bindings[0]);
                    break;
                case BindingType.WsHttpBinding:
                    _binding = new Bindings.WsHttpBinding(bindingSection.WSHttpBinding.Bindings[0]);
                    break;
                case BindingType.NetTCPBinding:
                    _binding = new Bindings.NetTcpBinding(bindingSection.NetTcpBinding.Bindings[0]);
                    break;
                case BindingType.AsmxBinding:
                    _binding = new Bindings.CustomAsmxBinding(bindingSection.CustomBinding.Bindings[0]);
                    break;
            }
        }

        private bool IsBaseType(Type type)
        {
            if (type == typeof (int))
            {
                return true;
            }
            if (type == typeof (double))
            {
                return true;
            }
            if (type == typeof (decimal))
            {
                return true;
            }
            if (type == typeof (bool))
            {
                return true;
            }
            if (type == typeof (string))
            {
                return true;
            }
            if (type == typeof (char))
            {
                return true;
            }
            return false;
        }


        private enum ValueType
        {
            Base,
            Object,
            Collection
        }

        private string[] GetFields(string value, ValueType type)
        {
            var returnValue = new List<string>();

            //Type is collection, format [value1,value2]
            //Return format is value1 | value2
            if(type == ValueType.Collection)
            {
                //Remove square braces
                value = value.Remove(0, 1).Remove(value.Count() - 2, 1);
                string field = string.Empty;
                var escape = new char[] { ':', ';', '[', ']', '{', '}', ',' };
                var openBraces = new List<char>();

                //Analyse the value by character
                foreach (char c in value)
                {
                    //Character is an escape character
                    if (escape.Any(val => val == c))
                    {
                        switch (c)
                        {
                            case ',':
                                    if(openBraces.Count == 0)
                                    {
                                        //All braces are closed, field is finish
                                        returnValue.Add(field);
                                        field = string.Empty;
                                        openBraces.Clear(); 
                                    }
                                    else
                                    {
                                        field += c;
                                    }
                                break;
                            case ':':
                                openBraces.Add(c);
                                field += c;
                                break;
                            case ';':
                                field += c;
                                openBraces.Remove(':');
                                break;
                            case '[':
                                field += c;
                                openBraces.Add('[');
                                break;
                            case '{':
                                field += c;
                                openBraces.Add('{');
                                break;
                            case '}':
                                field += c;
                                openBraces.Remove('{');
                                break;
                            case ']':
                                field += c;
                                openBraces.Remove('[');
                                break;
                        }
                    }
                    else
                    {
                        //If standard, take it
                        field += c;
                    }
                }

                //This line will add the last element that does not have a comma separation
                if(field != string.Empty && openBraces.Count == 0)
                    returnValue.Add(field);

                return returnValue.ToArray();
            }
            //Type is object, format {Property1: Value1; Property2: Value2;}
            //Return format is Property1: Value1; | Property2: Value2;
            else
            {
                value = value.Remove(0, 1).Remove((value.Count() - 2), 1).Trim();
                string field = string.Empty;
                var escape = new char[] {':', ';', '[', ']', '{', '}'};
                var openBraces = new List<char>();

                //Analyse the value by character
                foreach(char c in value)
                {
                    //Character is an escape character
                    if(escape.Any(val => val == c))
                    {
                        switch (c)
                        {
                            case ':':
                                    openBraces.Add(c);
                                    field += c;
                                    break;
                            case ';':
                                    field += c;
                                    openBraces.Remove(':');
                                    if(openBraces.Count == 0)
                                    {
                                        //All braces are closed, field is finish
                                        returnValue.Add(field);
                                        field = string.Empty;
                                        openBraces.Clear();
                                    }
                                break;
                            case '[':
                                    field += c;
                                    openBraces.Add('[');
                                break;
                            case '{':
                                    field += c;
                                    openBraces.Add('{');
                                break;
                            case '}':
                                field += c;
                                openBraces.Remove('{');
                                break;
                            case ']':
                                field += c;
                                openBraces.Remove('[');
                                break;
                        }
                    }
                    else
                    {
                        //If standard, take it
                        field += c;
                    }
                }

                return returnValue.ToArray();
            }
        }



    public void Dispose()
        {
            CloseClient();
        }
    }
}
